--[[	$Id: caelBagsSort.lua 2357 2011-12-05 12:57:46Z sdkyron@gmail.com $	]]

local _, caelBags = ...

caelBags.eventFrame = CreateFrame("Frame", nil, UIParent)

local debug = true
local process = nil

local skipToNext = function(bagId, slot)
   if GetContainerNumSlots(bagId) < slot then
      return true
   end

   if bagId < 1 then
      return false
   end

   local inventoryId = ContainerIDToInventoryID(bagId)
   local link = GetInventoryItemLink("player", inventoryId)

   if link == nil then
      return true
   end

   local class, subclass, _

   _, _, _, _, _, class, subclass = GetItemInfo(link)

   return class ~= "Container" or subclass ~= "Bag"
end

local getItem = function(bagId, slot)
   local item = {bag=bagId, slot=slot}
   local name, quality, class, subclass, stackCount, itemCount, _
   local itemId = GetContainerItemID(bagId, slot)

   if itemId ~= nil then
      _, itemCount, _, _, _ = GetContainerItemInfo(bagId, slot)
      name, _, quality, _, _, class, subclass, stackCount = GetItemInfo(itemId)
   end

   item.id = itemId
   item.name = name
   item.quality = quality
   item.class = class
   item.subclass = subclass
   item.stackCount = stackCount -- max in a stack
   item.itemCount = itemCount   -- amount in this stack

   return item
end

local iterBagContents = function(bags, bag, slot)
   local bagIndex = 1
   local slot = slot or 1

   for index = 1, #bags do
      if bags[index] == bag then
			bagIndex = index
      end
   end

   local iterator = function()
      local bagId = bags[bagIndex]

      while bagId ~= nil and skipToNext(bagId, slot) do
			bagIndex = bagIndex + 1
			bagId = bags[bagIndex]
			slot = 1
      end

      if bagId == nil then
			return nil
      end

      local item = getItem(bagId, slot)
      slot = slot + 1

      return item
   end

   return iterator
end


local cmp = function(x, y)
   if x ~= y then
      return x < y
   end
end

local cmpBag = function(item1, item2)
   return cmp(item2.bag, item1.bag) -- NB: Bag indexes are reversed!
end

local cmpSlot = function(item1, item2)
   return cmp(item1.slot, item2.slot)
end

local cmpItemCount = function(item1, item2)
   return cmp(item2.itemCount, item1.itemCount) -- NB: partial stacks last
end

local cmpName = function(item1, item2)
   return cmp(item1.name, item2.name)
end

local cmpQuality = function(item1, item2)
   return cmp(item1.quality, item2.quality)
end

local cmpClass = function(item1, item2)
   return cmp(item1.class, item2.class)
end

local cmpSubclass = function(item1, item2)
   return cmp(item1.subclass, item2.subclass)
end

local empty = function(item) -- 1 if slot is empty, else 0
   if item.name == nil then
      return 1
   end

   return 0
end

local cmpNotEmpty = function(item1, item2)
   return cmp(empty(item1), empty(item2))
end

local compare = function(item1, item2, comparisons)
   for i	=	1, #comparisons do
      local result = comparisons[i](item1, item2)

      if result ~= nil then
			return result
      end
   end
end

local compareItems = function(item1, item2)
   return compare(item1, item2, {cmpNotEmpty, cmpClass, cmpSubclass, cmpQuality, cmpName, cmpItemCount, cmpBag, cmpSlot})
end

local waitForLock = function(bag, slot)
   local locked = true

   while locked do
      local _
      _, _, locked = GetContainerItemInfo(bag, slot)

      if locked then
			coroutine.yield()
      end
   end
end

local findLowestAbove = function(bags, item)
   local lowest = item

   for cand in iterBagContents(bags, item.bag, item.slot + 1) do
      if compareItems(cand, lowest) then
			lowest = cand
      end
   end

   return lowest
end

local swap = function(item1, item2)
   if not item1.name then
      item1, item2 = item2, item1
   end

   if item1.name then
      PickupContainerItem(item1.bag, item1.slot)
      PickupContainerItem(item2.bag, item2.slot)
      waitForLock(item1.bag, item1.slot)
      waitForLock(item2.bag, item2.slot)
   end
end

local filterEmptyBagSlots = function(bags)
   local nonEmpty = {}

   for i	=	1, #bags do
      if GetContainerNumSlots(bags[i]) > 0 then
			table.insert(nonEmpty, bags[i])
      end
   end

   return nonEmpty
end

local stack = function(bags)
   local notFull = {}

   for item in iterBagContents(bags) do
      if item.id ~= nil then
			if item.stackCount > item.itemCount then
				if notFull[item.id] == nil then
					notFull[item.id] = {}
				end
				table.insert(notFull[item.id], item)
			end
      end
   end

   for _, items in pairs(notFull) do
      while #items > 1 do
			local source = items[#items]
			local destination = items[1]

			swap(source, destination)
			destination = getItem(destination.bag, destination.slot)
			source = getItem(source.bag, source.slot)

			if source.id == nil then
				table.remove(items) -- stack has disappeared
			end

			if destination.itemCount == destination.stackCount then
				table.remove(items, 1)
			end
		end
   end
end

local sort = function(bags)
   for item in iterBagContents(bags) do
      local lowest = findLowestAbove(bags, item)

      if lowest ~= item then
			swap(item, lowest)
      end
   end
end

local sortInventory = function()
   local bags = {4, 3, 2, 1, 0}
   stack(bags)
   sort(bags)
end

local sortBank = function()
   local bags = {11, 10, 9, 8, 7, 6, 5, -1}
   stack(bags)
   sort(bags)
end

local start = function(func, message)
   assert(not process)
   if message then
      print(message)
   end

   caelBags.eventFrame:SetScript("OnUpdate", function()
		success, status = coroutine.resume(process)

		if success and status ~= "done" then
			return
		end

		caelBags.eventFrame:SetScript("OnUpdate", nil)
		process = nil

		if success then
			print("Sorting done!")
		else
			print("Error while sorting")
			if debug then
				assert(success, status)
			end
		end
	end)

   function wrapper()
      func()

      return "done"
   end

   process = coroutine.create(wrapper)
end

SlashCmdList["SORT"] = function(self)
   if not process then
      if self == "bank" then
			start(sortBank, "Sorting bank")
		else
			start(sortInventory, "Sorting bags")
      end
   end
end

SLASH_SORT1 = "/sort"